
tinylang编译器(在前几章中开发的)没有对创建的IR代码进行优化。在接下来的部分中，我们将向编译器添加一个优化流水来精确地执行此操作。\par

\hspace*{\fill} \par %插入空行
\textbf{使用新Pass管理器创建一个优化流水}

设置优化流水的核心是PassBuilder类。这个类知道所有已注册的Pass，并可以根据文本描述构造Pass流水。我们使用这个类根据命令行上给出的描述创建Pass流水，或者根据请求的优化级别使用默认流水。我们还支持使用Pass插件，例如countir Pass插件。这样，就模拟了opt工具的部分功能，并为命令行选项使用类似的名称。\par

PassBuilder类填充了ModulePassManager类的实例，ModulePassManager类是保存已构造的Pass流水，并运行它的Pass管理器。代码生成Pass仍然使用旧Pass管理器，因此必须为此目的保留旧Pass管理器。 \par

为了实现功能，我们将tinylang编译器中的tools/driver/Driver.cpp文件进行了扩展:\par

\begin{enumerate}
\item 我们使用新的类，因此从添加新的包含文件开始。llvm/Passes/PassBuilder.h文件提供了PassBuilder类的定义。llvm/Passes/PassPlugin.h文件是插件支持所必需的。最后，llvm/An\allowbreak alysis/TargetTransformInfo.h文件提供了一个连接IR级转换和特定目标信息的Pass:
\begin{lstlisting}[caption={}]
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Analysis/TargetTransformInfo.h"
\end{lstlisting}

\item 为了使用新Pass管理器的某些特性，我们添加了三个命令行选项，它们的名称与opt工具相同。\verb|--|passes选项启用Pass管道的文本规范，而\verb|--|load-pass-plugin选项启用相应的Pass插件。如果给定了\verb|--|debug-pass-manager选项，Pass管理器将输出关于已执行的Pass的信息:
\begin{lstlisting}[caption={}]
static cl::opt<bool>
	DebugPM("debug-pass-manager", cl::Hidden,
			cl::desc("Print PM debugging 
					 information"));
static cl::opt<std::string> PassPipeline(
	"passes",
	cl::desc("A description of the pass pipeline"));
static cl::list<std::string> PassPlugins(
	"load-pass-plugin",
	cl::desc("Load passes from plugin library"));
\end{lstlisting}

\item 用户对优化程度的设定影响着对Pass流水的构建。PassBuilder类支持六种不同的优化级别:一种是不进行优化，三种是优化速度，两种是减小大小。我们可以使用命令行选项捕获所有这些级别:
\begin{lstlisting}[caption={}]
static cl::opt<signed char> OptLevel(
	cl::desc("Setting the optimization level:"),
	cl::ZeroOrMore,
	cl::values(
		clEnumValN(3, "O", "Equivalent to -O3"),
		clEnumValN(0, "O0", "Optimization level 0"),
		clEnumValN(1, "O1", "Optimization level 1"),
		clEnumValN(2, "O2", "Optimization level 2"),
		clEnumValN(3, "O3", "Optimization level 3"),
		clEnumValN(-1, "Os",
					"Like -O2 with extra 
					optimizations "
					"for size"),
		clEnumValN(
			-2, "Oz",
			"Like -Os but reduces code size further")),
	cl::init(0));
\end{lstlisting}

\item LLVM的插件机制支持静态插件注册表，它是在项目配置期间创建的。为了使用这个注册表，包含了llvm/Support/Extension.def数据库文件来创建函数的原型，并返回插件信息:
\begin{lstlisting}[caption={}]
#define HANDLE_EXTENSION(Ext) \
	llvm::PassPluginLibraryInfo get##Ext##PluginInfo();
#include "llvm/Support/Extension.def"
\end{lstlisting}

\item 我们将重新实现emit()函数。函数的顶部声明了必需的PassBuilder实例:
\begin{lstlisting}[caption={}]
bool emit(StringRef Argv0, llvm::Module *M,
			llvm::TargetMachine *TM,
			StringRef InputFilename) {
	PassBuilder PB(TM);
\end{lstlisting}

\item 为了实现对命令行中给定的Pass插件支持，我们循环遍历用户给出的插件库列表，并尝试加载插件。如果失败，会发出一个错误消息;否则，就注册Pass:
\begin{lstlisting}[caption={}]
	for (auto &PluginFN : PassPlugins) {
		auto PassPlugin = PassPlugin::Load(PluginFN);
		if (!PassPlugin) {
			WithColor::error(errs(), Argv0)
				<< "Failed to load passes from '" 
				<< PluginFN
				<< "'. Request ignored.\n";
			continue;
		}
		PassPlugin->registerPassBuilderCallbacks(PB);
	}
\end{lstlisting}

\item 静态插件注册表中的信息以类似的方式用于在PassBuilder实例中注册这些插件:
\begin{lstlisting}[caption={}]
#define HANDLE_EXTENSION(Ext) \
	get##Ext##PluginInfo().RegisterPassBuilderCallbacks( \
		PB);
#include "llvm/Support/Extension.def"
\end{lstlisting}

\item 我们需要为不同的分析管理器声明变量。唯一的参数是调试标志:
\begin{lstlisting}[caption={}]
	LoopAnalysisManager LAM(DebugPM);
	FunctionAnalysisManager FAM(DebugPM);
	CGSCCAnalysisManager CGAM(DebugPM);
	ModuleAnalysisManager MAM(DebugPM);
\end{lstlisting}

\item 接下来，我们将调用PassBuilder实例上相应的注册方法来填充分析管理器。通过这个调用，分析管理器将填充默认的分析Pass，并运行注册回调。还需要确保函数分析管理器使用默认的别名-分析管道，并且所有的分析管理器都互相知道:
\begin{lstlisting}[caption={}]
	FAM.registerPass(
		[&] { return PB.buildDefaultAAPipeline(); });
	PB.registerModuleAnalyses(MAM);
	PB.registerCGSCCAnalyses(CGAM);
	PB.registerFunctionAnalyses(FAM);
	PB.registerLoopAnalyses(LAM);
	PB.crossRegisterProxies(LAM, FAM, CGAM, MAM);
\end{lstlisting}

\item MPM模块Pass管理器保存着我们构造的Pass流水。实例用debug标志初始化:
\begin{lstlisting}[caption={}]
	ModulePassManager MPM(DebugPM);
\end{lstlisting}

\item 我们实现了两种不同的方法来使用Pass流水填充模块Pass管理器。如果用户在命令行上提供了一个Pass流水，也就是说，使用了\verb|--|passes选项，那么就使用这个作为Pass流水:
\begin{lstlisting}[caption={}]
	if (!PassPipeline.empty()) {
		if (auto Err = PB.parsePassPipeline(
		MPM, PassPipeline)) {
			WithColor::error(errs(), Argv0)
			<< toString(std::move(Err)) << "\n";
			return false;
		}
	}
\end{lstlisting}

\item 否则，根据使用所选的优化级别来确定要构造的Pass管道。默认Pass管道的名称为default，它会将优化级别作为参数:
\begin{lstlisting}[caption={}]
	else {
		StringRef DefaultPass;
		switch (OptLevel) {
			case 0: DefaultPass = "default<O0>"; break;
			case 1: DefaultPass = "default<O1>"; break;
			case 2: DefaultPass = "default<O2>"; break;
			case 3: DefaultPass = "default<O3>"; break;
			case -1: DefaultPass = "default<Os>"; break;
			case -2: DefaultPass = "default<Oz>"; break;
		}
		if (auto Err = PB.parsePassPipeline(
				MPM, DefaultPass)) {
			WithColor::error(errs(), Argv0)
				<< toString(std::move(Err)) << "\n";
			return false;
		}
	}
\end{lstlisting}

\item 现在已经建立了在IR代码上运行转换的Pass流水。我们需要一个打开的文件来写入结果。系统汇编器和LLVM IR输出都是基于文本的，所以应该为它们都设置OF\underline{~}text标志:
\begin{lstlisting}[caption={}]
	std::error_code EC;
	sys::fs::OpenFlags OpenFlags = sys::fs::OF_None;
	CodeGenFileType FileType = codegen::getFileType();
	if (FileType == CGFT_AssemblyFile)
		OpenFlags |= sys::fs::OF_Text;
	auto Out = std::make_unique<llvm::ToolOutputFile>(
		outputFilename(InputFilename), EC, OpenFlags);
	if (EC) {
		WithColor::error(errs(), Argv0)
			<< EC.message() << '\n';
		return false;
	}
\end{lstlisting}

\item 对于代码生成，必须使用旧Pass管理器。我们只需声明CodeGenPM实例，并添加Pass，使特定于目标的信息在IR转换级别可用:
\begin{lstlisting}[caption={}]
	legacy::PassManager CodeGenPM;
	CodeGenPM.add(createTargetTransformInfoWrapperPass(
		TM->getTargetIRAnalysis()));
\end{lstlisting}

\item 为了输出LLVM IR，添加了一个Pass，只是将IR打印到流中:
\begin{lstlisting}[caption={}]
	if (FileType == CGFT_AssemblyFile && EmitLLVM) {
		CodeGenPM.add(createPrintModulePass(Out->os()));
	}
\end{lstlisting}

\item 否则，我们让TargetMachine实例添加所需的代码生成Pass，由作为参数传递的FileType值指导:
\begin{lstlisting}[caption={}]
	 else {
		if (TM->addPassesToEmitFile(CodeGenPM, Out->os(),
		nullptr, FileType)) {
			WithColor::error()
				<< "No support for file type\n";
			return false;
		}
	}
\end{lstlisting}

\item 在所有这些准备之后，现在就可以执行Pass了。首先，在IR模块上运行优化流水。接下来，运行代码生成Pass。当然，在所有这些工作之后，我们希望保留输出文件:
\begin{lstlisting}[caption={}]
	MPM.run(*M, MAM);
	CodeGenPM.run(*M);
	Out->keep();
	return true;
}
\end{lstlisting}

\item 代码很多，但很简单。当然，还必须更新tools/driver/CMakeLists.txt构建文件中的依赖项。除了添加目标组件外，还添加了来自LLVM的所有转换和代码生成组件。这些名称与源代码所在的目录名称大致相似。组件名在配置过程中会转换为链接库的名称:
\begin{tcolorbox}[colback=white,colframe=black]
set(LLVM\underline{~}LINK\underline{~}COMPONENTS \$\{LLVM\underline{~}TARGETS\underline{~}TO\underline{~}BUILD\} \\
\hspace*{0.5cm}AggressiveInstCombine Analysis AsmParser \\
\hspace*{0.5cm}BitWriter CodeGen Core Coroutines IPO IRReader \\
\hspace*{0.5cm}InstCombine Instrumentation MC ObjCARCOpts Remarks \\
\hspace*{0.5cm}ScalarOpts Support Target TransformUtils Vectorize \\
\hspace*{0.5cm}Passes)
\end{tcolorbox}

\item 我们的编译器驱动支持插件，可以声明支持插件:
\begin{tcolorbox}[colback=white,colframe=black]
add\underline{~}tinylang\underline{~}tool(tinylang Driver.cpp SUPPORT\underline{~}PLUGINS)
\end{tcolorbox}

\item 和以前一样，必须链接我们自己的库:
\begin{tcolorbox}[colback=white,colframe=black]
target\underline{~}link\underline{~}libraries(tinylang
\hspace*{0.5cm}PRIVATE tinylangBasic tinylangCodeGen
\hspace*{0.5cm}tinylangLexer tinylangParser tinylangSema)
\end{tcolorbox}
这些都需要添加到源代码和构建系统中的。

\item 要构建扩展的编译器，请切换到构建目录并输入以下内容:
\begin{tcolorbox}[colback=white,colframe=black]
\$ ninja
\end{tcolorbox}
\end{enumerate}

对构建系统文件的更改会自动检测到，在编译和链接更改的源代码之前，运行cmake。如果您需要重新运行配置步骤，请按照第2章的步骤，编译tinylang。\par

既然已经将opt工具的选项作为蓝图使用，那么应该尝试运行tinylang来加载Pass插件并运行Pass，就像在前几节中所做的那样。\par

使用当前的实现，我们既可以运行默认的Pass流水，也可以自己构造一个。后者是非常灵活的。在所有情况下，都是多余的。默认流水在类C语言中运行得非常好。缺少的是一种扩展Pass管道的方法。在下一节中，我们将解释如何实现它。\par



\hspace*{\fill} \par %插入空行
\textbf{扩展Pass流水}


在前一节中，可以通过用户提供的描述或预定义的名称，使用PassBuilder类创建了Pass流水。现在，我们来了解一下定制Pass流水的另一种方法:使用扩展点。\par

在构造Pass流水期间，Pass构建器允许您添加由用户贡献的Pass。这些位置称为扩展点。LLVM有许多扩展点，如:\par

\begin{itemize}
\item 流起始扩展点允许您在管道的开头添加Pass。
\item 窥视孔扩展点允许您在指令组合符Pass的每个实例之后添加Pass。
\end{itemize}

还有其他扩展点。要使用扩展点，需要注册一个回调。在构造Pass流水期间，回调将在定义的扩展点运行，并可以向给定的Pass管理器添加一个Pass。\par

要为流水起始扩展点注册回调，可以调用PassBuilder类的registerPipelineStartEPCallback()方法。例如，将CountIRPass Pass添加到流水的开头，需要通过调用createModuleToFunctionPass\allowbreak Adaptor()模板函数来调整Pass作为模块Pass使用，然后将Pass添加到模块Pass管理器中:\par

\begin{lstlisting}[caption={}]
PB.registerPipelineStartEPCallback(
	[](ModulePassManager &MPM) {
		MPM.addPass(
		createModuleToFunctionPassAdaptor(
		CountIRPass());
	});
\end{lstlisting}

可以在Pass流水创建之前的任何时刻，也就是调用parsePassPipeline()方法之前，在Pass流水设置代码中添加此代码。\par

对于上一节中所做的工作，一个非常自然的扩展是让用户在命令行上传递一个扩展点的Pass流水描述(opt工具也允许这样做)。先为流水起始扩展点这样做。首先，将以下代码添加到tools/driver/\allowbreak Driver.cpp文件中:\par

\begin{enumerate}
\item 为用户添加了一个新的命令行来指定管道描述。同样，从opt工具中获取选项名:
\begin{lstlisting}[caption={}]
static cl::opt<std::string> PipelineStartEPPipeline(
	"passes-ep-pipeline-start",
	cl::desc("Pipeline start extension point));
\end{lstlisting}

\item 使用Lambda函数作为回调函数是最方便的方法。为了解析流水描述，调用PassBuilder实例的parsepaspipeline()方法。Pass会添加到PM Pass管理器，并作为Lambda函数的参数给出。如果出现错误，则在不停止应用程序的情况下打印错误消息。可以在调用crossRegisterProxies()方法之后添加以下代码:
\begin{lstlisting}[caption={}]
PB.registerPipelineStartEPCallback(
[&PB, Argv0](ModulePassManager &PM) {
	if (auto Err = PB.parsePassPipeline(
				PM, PipelineStartEPPipeline)) {
		WithColor::error(errs(), Argv0)
		<< "Could not parse pipeline "
		<< PipelineStartEPPipeline.ArgStr 
		<< ": "
		<< toString(std::move(Err)) << "\n";
	}
});
\end{lstlisting}
\begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=Tip]
要允许用户在每个扩展点添加Pass，需要为每个扩展点添加前面的代码。
\end{tcolorbox}

\item 现在是尝试不同Pass管理器的好时机。使用\verb|--|debug-pass-manager选项，可以按照顺序执行哪些pass。可以使用\verb|--|print-before-all和\verb|--|print-after-all选项在调用每个Pass之前或之后打印IR。如果创建了自己的Pass流水，那么可以将打印Pass插入到感兴趣的点中。例如，尝试一下\verb|--|passes="print,inline,print"选项。还可以使用print Pass来探索各种扩展点。

\begin{tcolorbox}[colback=blue!5!white,colframe=mymauve!75!black, title=LLVM 12的新打印选项]
LLVM 12支持-print-changed选项，与之前Pass的结果相比，该选项仅在IR代码发生更改时打印。大大减少的输出，使得跟踪IR转换更加容易。
\end{tcolorbox}

PassBuilder类有一个嵌套的OptimizationLevel类来表示六个不同的优化级别。而不是使用"default<O?>"流水描述作为parsepaspipeline()方法的参数，我们也可以调用buildPer\allowbreak ModuleDefaultPipeline()方法，为优化级别构建相应的优化管道——除了级别O0。优化级别为O0，表示不执行优化。因此，Pass管理器中不会添加任何Pass。如果我们仍然想运行某个Pass，可以手动将它添加到Pass管理器中。这个级别运行的是一个简单的Pass：AlwaysInliner Pass，它将一个标记有always\underline{~}内联属性的函数内联到调用者中。将优化级别的命令行选项值转换为OptimizationLevel类的相应成员，实现如下:
\begin{lstlisting}[caption={}]
	PassBuilder::OptimizationLevel Olevel = …;
	if (OLevel == PassBuilder::OptimizationLevel::O0)
		MPM.addPass(AlwaysInlinerPass());
	else
		MPM = PB.buildPerModuleDefaultPipeline(OLevel, 
			DebugPM);
\end{lstlisting}

当然，可以通过这种方式向Pass管理器添加多个Pass。PassBuilder类还在构造Pass流水期间使用addPass()方法。\par

\begin{tcolorbox}[colback=blue!5!white,colframe=mymauve!75!black, title=LLVM 12中的新功能——运行扩展点回调]
因为Pass流水不是为优化级别O0填充的，所以不会调用注册的扩展点。如果使用扩展点来注册Pass(它也应该在O0级别运行)，那么这就有问题了。在LLVM 12中，可以调用新的runRegisteredEPCallbacks()方法来运行注册的扩展点回调，导致Pass管理器只填充通过扩展点注册的Pass。
\end{tcolorbox}

\end{enumerate}

通过向tinylang添加优化流水，您可以创建一个优化编译器，比如Clang。LLVM社区致力于改进每个版本的优化和优化流水。因此，很少使用默认流水。大多数情况下，添加新Pass是为了实现编程语言的某些语义。\par




